package com.zxq.common.middleware.lock;


import com.google.common.base.Preconditions;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

/**
 * 基于 Redis 的重入分布式锁锁实现
 */
@Slf4j
public class RedisLock implements Lock {


    public RedisLock(String lockName, RedisProcessor redisProcessor, final RenewalWatchdog watchdog, int expireTime, int retry) {
        this.redisProcessor = redisProcessor;
        this.key = new StoreKey(redisProcessor.getCategory(), lockName);
        this.lockName = lockName;
        this.expireTime = expireTime;
        this.retry = retry;
        this.uuid = UUID.randomUUID().toString();

        this.watchdog = watchdog;
    }

    @Override
    public String getName() {
        return lockName;
    }

    @Override
    public boolean tryLock() {
        // 锁重入检查
        if (reentrant()) {
            return true;
        }

        try {
            if (redisProcessor.setNx(key, uuid, expireTime, retry)) {
                lockAtOutermost();

                log.info("Try acquire lock success. key: {}, uuid: {}", key, uuid);
                return true;
            } else {
                log.debug("Try acquire lock failed. key: {}, uuid: {}", key, uuid);
                return false;
            }
        } catch (Exception e) {
            log.error("Redis try acquire lock failed, key: {}", key, e);

            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {

        Preconditions.checkNotNull(unit, "时间单位不能为空");

        // 锁重入检查
        if (reentrant()) {
            log.info("Reentrant lock success. key: {}, uuid: {}", key, uuid);
            return true;
        }

        long timeout = System.nanoTime() + unit.toNanos(waitTime);
        if (timeout < 0) {
            timeout = Long.MAX_VALUE;
        }
        while (true) {
            try {
                if (redisProcessor.setNx(key, uuid, expireTime, retry)) {
                    // locks.set(new LockHolder(key.toString()));
                    lockAtOutermost();

                    log.info("Try acquire lock success. key: {}, uuid: {}", key, uuid);

                    return true;
                }
            } catch (InterruptedException e) {
                throw new InterruptedException();
            } catch (Exception e) {
                log.error("Redis try acquire lock failed, key: {}", key, e);
                throw new RuntimeException(e);
            }

            if (System.nanoTime() >= timeout) {
                log.debug("Try acquire lock timeout. key: {}, uuid: {}", key, uuid);
                return false;
            } else {
                log.debug("Try acquire lock failed, will try again. key: {}, uuid: {}", key, uuid);
            }

            LockSupport.parkNanos(this, TimeUnit.MILLISECONDS.toNanos(DistributeLockConstants.RedisLockConstants.SPIN_AWAIT_TIME));

            if (Thread.interrupted()) {
                throw new InterruptedException();
            }
        }

    }

    @Override
    public void unlock() {
        LockHolder lockHolder = this.locks.get();
        if (lockHolder == null) {
            throw new IllegalMonitorStateException("Attempting to unlock without first obtaining that lock on this thread");
        }

        int lockCounts = lockHolder.decrementLock();
        try {
            if (lockCounts == 0) {
                unlockAtOutermost();

                if (redisProcessor.compareAndDelete(key, uuid, retry)) {
                    log.info("Release lock success. key: {}, uuid: {}", key, uuid);
                } else {
                    log.debug("Release lock failed. key: {}, uuid: {}", key, uuid);
                }
            }
        } catch (Exception e) {
            log.error("Redis try release lock failed. key: {}", key, e);
            throw new RuntimeException(e);
        }
    }


    protected void unlockAsync() {
        LockHolder lockHolder = this.locks.get();
        if (lockHolder == null) {
            throw new IllegalMonitorStateException("Attempting to unlock without first obtaining that lock on this thread");
        }

        int lockCounts = lockHolder.decrementLock();

        if (lockCounts > 0) {
            return;
        }

        // locks.remove();
        unlockAtOutermost();

        log.debug("Async unlock: {}", getName());
        ThreadPoolUtils.getMultiLockExecutor().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    if (redisProcessor.compareAndDelete(key, uuid, retry)) {
                        log.debug("Release lock success. key: {}, uuid: {}", key, uuid);
                    } else {
                        log.debug("Release lock failed. key: {}, uuid: {}", key, uuid);
                    }
                } catch (Exception e) {
                    log.error("Redis try release lock failed. key: {}", key, e);
                    throw new RuntimeException(e);
                }
            }
        });
    }



    ///////////////////////////////////////////////////////////////////////

    private RenewalWatchdog watchdog;

    @Getter(AccessLevel.PACKAGE)
    private RedisProcessor redisProcessor;

    /**
     * 锁名称
     */
    private final String lockName;
    /**
     * Redis 中的键
     */
    @Getter(AccessLevel.PACKAGE)
    private final StoreKey key;
    /**
     * 自动解锁时间, 单位秒
     */
    @Getter(AccessLevel.PACKAGE)
    private final int expireTime;
    /**
     * UUID
     */
    @Getter(AccessLevel.PACKAGE)
    private final String uuid;
    /**
     * 重试次数
     */
    private final int retry;


    /**
     * 线程本地变量, 用于锁重入检查
     */
    private ThreadLocal<LockHolder> locks = new ThreadLocal<>();

    /**
     * 锁重入检查
     *
     * @return 重入则返回 true; 否则返回 false
     */
    private boolean reentrant() {
        try {
            if (LockHolder.checkReentrancy(locks)) {
                // reentrant, refresh lease time
                // return redisProcessor.compareAndSet(key, this.uuid, this.uuid, expireTime, retry);
                watchdog.watch(this);

                return true;
            } else {
                return false;
            }
        } catch (Exception e) {
            throw new RuntimeException("Redis refresh lease time failed, key: " + key, e);
        }
    }

    protected void lockAtOutermost() {
        locks.set(new LockHolder(key.toString()));
        watchdog.watch(this);
    }

    protected void unlockAtOutermost() {
        locks.remove();
        watchdog.unWatch(this);
    }

}
